// scaup - Scala up!
// (c) 2008-2010, Normen Müller, normen.mueller@gmail.com  
// $Id: files.scala 78 2010-05-13 12:25:41Z normen.mueller $
package scaup.io

import java.io._
import java.nio.channels._
import java.text.MessageFormat.{format}

import scaup.io.jna.JNAUtil
import scaup.Views._

/** Adds extra methods to <code>java.io.File</code>.
 * 
 *  @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 *  @version 0.0.4
 * 
 *  @param file to enclose/ extend
 */
private [scaup] class FileExtras(file: File) extends PartiallyOrdered[File] {
  import FileType._
  
  if (file == null)
    error("File may not be null")
  
  /** @see <code><a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#getName()">getName()</a></code>.
   */
  lazy val name = file.getName()
  
  /** File extension
   */
  lazy val ext = {
    val lastDot = name.lastIndexOf('.')
    val lastDirSep = name.lastIndexOf(File.separatorChar)
    if(!((lastDot == -1 || lastDirSep != -1 && lastDot < lastDirSep)))
      Some(name.substring(lastDot + 1).toLowerCase())
    else None 
  }
  
  /** File name without extension
   */
  lazy val label = {
    val lastDot = name.lastIndexOf('.')
    val lastDirSep = name.lastIndexOf(File.separatorChar)
    if(lastDot == -1)
      name.substring(lastDirSep + 1).toLowerCase()
    else name.substring(lastDirSep + 1, lastDot).toLowerCase()
  }

  /** @see <code><a href="#path">path</a></code>.
   */
  lazy val p = path
  
  /** @see <code><a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#getName()">getName()</a></code>.
   */
  lazy val path = file.getPath()
  
  /** @see <code><a href="#absolutePath">absolutePath</a></code>.
   */
  lazy val ap = absolutePath
  
  /** @see <code><a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#getAbsolutePath()">getAbsolutePath()</a></code>.
   */
  lazy val absolutePath = file.getAbsolutePath()
  
  /** @see <code><a href="#canonicalPath">canonicalPath</a></code>.
   */
  lazy val cp = canonicalPath
  
  /** @see <code><a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#getCanonicalPath()">getCanonicalPath()</a></code>.
   */
  lazy val canonicalPath = file.getCanonicalPath()
  
  lazy val resolvedPath = new java.net.URL(System.getProperty("user.dir").toFile.toURL, file.path).getPath
    
  /** @see <code><a href="#absoluteFile">absoluteFile</a></code>.
   */
  lazy val af = absoluteFile
  
  /** @see <code><a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#getAbsoluteFile()">getAbsoluteFile()</a></code>.
   */
  lazy val absoluteFile = file.getAbsoluteFile()
  
  /** @see <code><a href="#canonicalFile">canonicalFile</a></code>.
   */
  lazy val cf = canonicalFile
  
  /** @see <code><a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#getCanonicalFile()">getCanonicalFile()</a></code>.
   */  
  lazy val canonicalFile = file.getCanonicalFile()
  
  lazy val resolvedFile = resolvedPath.toFile
  
  /** @see <code><a href="#parentPath">parentPath</a></code>.
   */
  lazy val pp = parentPath
  
  /** @see <code><a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#getParent()">getParent()</a></code>.
   */
  lazy val parentPath = file.getParent()
  
  /** @see <code><a href="#parentFile">parentFile</a></code>.
   */
  lazy val pf = parentFile
  
  /** @see <code><a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#getParentFile()">getParentFile()</a></code>.
   */
  lazy val parentFile = file.getParentFile()
  
  /** Returns the file type.
   *  
   *  @see <code><a href="#FileType">FileType</a></code>.
   */
  lazy val typ = JNAUtil.getFileType(file)
  
  /** Returns some link target; otherwise none.
   */
  lazy val lnktgt = JNAUtil.getLinkTarget(file)
  
  /** Returns a list of the path components.
   */
  lazy val toList = file.p.replace(File.separatorChar, '/').split('/').foldLeft (Nil: List[String])((l,d) => l::: List(d))
  
  /** Returns a list of directory content if <code>file</code> is a directory; otherwise Nil 
   */
  lazy val ls = if(file.exists && (file.typ == DIRECTORY || (file.typ == SYMLINK && file.cf.typ == DIRECTORY))) file.listFiles.toList else Nil
  
  /** Returns a list of directory content recursively if <code>file</code> is a directory; otherwise Nil
   */
  lazy val lsR: List[File] = ((Nil: List[File]) /: (for(d <- file.ls) yield d :: d.lsR))((l,d) => l ::: d)
  
  /** Slurp file content into a string.
   */
  lazy val content = FileUtil.readFile(file)
  
  /** Returns an input stream.
   */
  lazy val inStream = new FileInputStream(file)
  
  /** Returns an output stream.
   */
  lazy val outStream = FileUtil.createFileOutputStream(file)
  
  /** Create an empty file named by this <code>file</code> abstract pathname.
   * 
   *  @return Some empty file; otherwise none
   */
  def touch() = FileUtil.createEmptyFile(file)
  
  /** @see <code><a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#File(java.io.File,%20java.lang.String)">File(File, String)</a></code>.
   */
  def /(child: String) = new File(file, child)
  
  /** Create a symbolic link to <code>link</code> target.
   * 
   *  <p><strong>Note:</strong> If <code>file</file> already exists, <code>file</code> is deleted and
   *  re-created as symbolic link no matter <code>link</code> is a valid target.</p>
   * 
   *  @return the symbolic link wrapped in an option if succeeded, otherwise none
   */
  def linkTo(link: String): Option[File] = JNAUtil.createSymlink(file.remove(), link)
  
  /** Unzips the file into the specified directory.
   */
  def unzipTo(outdir: File) = StreamUtil.unzip(new FileInputStream(file), outdir)
  
  /**
   *  @note Does not resolve symbolic links.
   */
  def relativePathFrom(path: File) = (path.toList --- file.toList).map(e => ".." + File.separator).mkString + ((file.toList --- path.toList) mkString File.separator)
  
  /**
   * @note Does not resolve symbolic links.
   */
  def relativePathTo(path: File) = (file.toList --- path.toList).map(e => ".." + File.separator).mkString + ((path.toList --- file.toList) mkString File.separator)
  
  def resolveWrt(path: File) = new java.net.URL(path.toURL, file.path).getPath
  
  def tryCompareTo [B >: File <% PartiallyOrdered[B]](that: B): Option[Int] = that match {
    case x: File =>
      if(file.toList == x.toList)
        Some(0)
      else if(file.toList startsWith x.toList)
        Some(-1)
      else if(x.toList startsWith file.toList)
        Some(1)
      else None
    case _ => None
  }

  /* ================== @fixme entirely reassess subsequent methods ================================================ */
  
  /**
   * @todo document
   * @return if succeeded dst wrapped in an option, otherwise none
   */
  def copyTo(dst: Option[File]): Option[File] = dst match {case Some(d) => copyTo(d) case _ => None}
  def copyTo(dst: File): Option[File] = FileUtil.copy(file, dst, false)
  
  /**
   * @todo document
   * @return if succeeded dst wrapped in an option, otherwise none
   */
  def copySaveTo(dst: File): Option[File] = FileUtil.copy(file, dst, true)
  
  /**
   * @todo document
   * @return if succeeded dst wrapped in an option, otherwise none
   */
  def renameTo(dst: File): Option[File] = FileUtil.rename(file, dst)
  
  /**
   * @todo document
   * @return if succeeded file wrapped in an option, otherwise none
   */
  def remove(): Option[File] = FileUtil.delete(file)
  
  /**
   * @todo document
   * Note: Doesn't reliable work on each platform. Seems to be founded in JNA.
   */
  def isExecutable = JNAUtil.isExecutable(file)
  
  /**
   * @todo document
   */
  def setExecutable(set: Boolean) = 
    if(JNAUtil.setExecutable(file, set))
      true
    else if (isWriteable && set)
      Runtime.getRuntime().exec(Array[String]("chmod",  "ugo+x", absolutePath)).waitFor
    else if (isWriteable)
      Runtime.getRuntime().exec(Array[String]("chmod",  "ugo-x", absolutePath)).waitFor
  
  /**
   * @todo document
   */
  def isWriteable = file.canWrite
  
  /**
   * @todo document
   * Note: Doesn't reliable work on each platform. Seems to be founded in JNA.
   */
  def setWriteable(set: Boolean) = JNAUtil.setWritable(file, set)
  
  /**
   * @todo document
   * Note: Doesn't reliable work on each platform. Seems to be founded in JNA.
   * @return if succeeded file wrapped in an option, otherwise none
   */
  def setSGID = JNAUtil.setSGID(file)
}

/** Common file types.
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.4 */
object FileType extends Enumeration {  
  val UNKNOWN = Value("unknown")
  val NONE = Value("none")
  val FILE = Value("file")
  val SYMLINK = Value("symlink")
  val DIRECTORY = Value("directory")  
}

/** Shortcuts for file operations.
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.4
 * @fixme reassess entirely
 */
private[io] object FileUtil {
  import FileType._
  
  private val STREAM_CHUNK_SIZE = 16384
    
  def copy(src: File, dst: File, safe: Boolean): Option[File] = src.typ match {
    case FILE => copyFile(src, dst, safe)    
    case SYMLINK => src.lnktgt match {
      case Some(tgt) => dst linkTo tgt
      case None => None
    }       
    case DIRECTORY =>
      if(!dst.exists) {
        dst.mkdirs
        dst setLastModified src.lastModified
      }
      src.ls filter (f => f.name != ".." && f.name != "." && f != dst) map (f => copy(f, dst/f.name, safe))
      Some(dst)
    case _ => None //error(format("Cannot copy file ''{0}'' to ''{1}''; file ''{0}'' has unknown type", src, dst))
  }
  
  def rename(src: File, dst: File): Option[File] =
    if (src.typ != NONE)
      if (!dst.isDirectory)
        if(src renameTo dst) Some(dst) else None
      else error(format("Cannot rename file ''{0}'' to ''{1}''; file ''{1}'' is a directory", src, dst))     
    else dst.remove
  
  def delete(file: File): Option[File] = file.typ match {
    case FILE | SYMLINK => if(file.delete) Some(file) else None
    case DIRECTORY => if(deleteAll(file, true)) Some(file) else None
    case _ => Some(file)
  }
  
  def createFileOutputStream(file: File): FileOutputStream = createFileOutputStream(file, false)
  def createFileOutputStream(file: File, append: Boolean): FileOutputStream = new FileOutputStream(file, append)
  
  def readFile(file: File): String = {
    var is: InputStream = null
    try {
      is = openFileForReading(file)
      readFile(is)
    } catch {
      case _ => null
    } finally { is.close() }
  }  
  def readFile(input: InputStream): String = {
    //@todo make it more functional
    val buf = new Array[Byte](STREAM_CHUNK_SIZE)
    val result = new StringBuilder
    var r: Int = -1
    do {
      r = input read buf
      if(r != -1)
        result append new String(buf, 0, r, "UTF-8")
    } while(r != -1)
    result.toString
  }
//  private static String readFileAsString(String filePath) throws java.io.IOException {
//        StringBuffer fileData = new StringBuffer(1000);
//        BufferedReader reader = new BufferedReader(new FileReader(filePath));
//        char[] buf = new char[1024];
//        int numRead = 0;
//        while ((numRead = reader.read(buf)) != -1) {
//            String readData = String.valueOf(buf, 0, numRead);
//            fileData.append(readData);
//            buf = new char[1024];
//        }
//        reader.close();
//        return fileData.toString();
//    }
  
  def createUniqueFile(parent: File, name: String, suffix: String): Option[File] = {
    import java.util.UUID
    def create(file: File): Option[File] =
      if(file.typ == FileType.NONE) file.touch()
      else create(parent / (new StringBuilder(name) append "." append UUID.randomUUID().toString append suffix).toString)
    create(parent / (new StringBuilder(name) append "." append UUID.randomUUID().toString append suffix).toString)
  }
  
  def createEmptyFile(file: File): Option[File] = {
    if (file != null && file.pf != null && !file.pf.exists)
      file.pf.mkdirs
    if(file != null)
      if(file.createNewFile())
        Some(file)
      else None
    else None
  }
  
  private def copyFile(src: File, dst: File, safe: Boolean): Option[File] =
    if(src == null || dst == null) None
    else if (src == dst) Some(dst)
    else if (!(src exists)) dst.remove()
    else {
      var tmpDst = dst
      if (dst.typ != NONE)
        if (safe) tmpDst = createUniqueFile(dst.pf, ".copy", ".tmp").getOrElse(null)
        else dst.remove()
      if(tmpDst != null) {
	      val executable = src.isExecutable
	      dst.pf.mkdirs          
	      var srcChannel: FileChannel = null
	      var dstChannel: FileChannel = null
	      var is: FileInputStream = null
	      var os: FileOutputStream = null          
	      var error: Error = null          
	      try {
	        is = src.inStream
	        srcChannel = is.getChannel
	        os = tmpDst.outStream
	        dstChannel = os.getChannel
	        val totalSize = srcChannel.size
	        var toCopy = totalSize
	        while (toCopy > 0) { toCopy -= dstChannel.transferFrom(srcChannel, totalSize - toCopy, toCopy) }
	      } catch { case e @ _ => error = new Error(format("Cannot copy file ''{0}'' to ''{1}'': {2}", src, dst, e.getLocalizedMessage))
	      } finally {
	        if (srcChannel != null) srcChannel.close
	        if (dstChannel != null) dstChannel.close
	        is.close
	        os.close
	      }          
	      if(error != null) {
	        error = null
	        var sis: InputStream = null
	        var dos: OutputStream = null
	        try {
	          sis = openFileForReading(src)
	          dos = openFileForWriting(dst)
	          copy(sis, dos)
	        } catch { case e @ _ => error = new Error(format("Cannot copy file ''{0}'' to ''{1}'': {2}", src, dst, e.getLocalizedMessage))
	        } finally {
	          dos.close
	          sis.close
	        }            
	      }          
	      if (error != null) throw error          
	      if (safe && tmpDst != dst) tmpDst renameTo dst        
	      if (executable) dst setExecutable true        
	      dst setLastModified src.lastModified
	      Some(dst)
      } else None
    }
  
  private def copy(src: InputStream, dst: OutputStream): Unit = {
    val buffer = new Array[Byte](8192)
    while (true) {
      val read = src read buffer
      if (read <= 0) return
      dst.write(buffer, 0, read)
    }
  }
  
  private def deleteAll(file: File, state: Boolean): Boolean = if(file != null) (state /: file.ls)((s, f) => s && deleteAll(f, state)) && file.delete else state
  
  private def openFileForReading(file: File): InputStream = if (file != null) try { new BufferedInputStream(file.inStream) } catch { case _ => null } else null
  
  private def openFileForWriting(file: File): OutputStream = openFileForWriting(file, false)  
  private def openFileForWriting(file: File, append: Boolean): OutputStream =
    if(file != null) {
      if(file.pf != null && !file.pf.exists) file.pf.mkdirs
      if(file.isFile() && !file.canWrite()) file setWriteable true
      try { new BufferedOutputStream(createFileOutputStream(file, append)) } catch { case _ => null }
    } else null  
}
